// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.search.ui.results

import android.content.Context
import android.content.Intent
import android.location.Location
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.toSet
import kotlinx.coroutines.runBlocking
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.data.settings.SettingsRepository
import xyz.apiote.bimba.czwek.data.traffic.PointOfInterest
import xyz.apiote.bimba.czwek.departures.DeparturesActivity
import xyz.apiote.bimba.czwek.repo.FeedInfo
import xyz.apiote.bimba.czwek.repo.Line
import xyz.apiote.bimba.czwek.repo.Queryable
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.StopStub
import xyz.apiote.bimba.czwek.search.LineGraphActivity
import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
import xyz.apiote.bimba.czwek.units.Metre
import xyz.apiote.bimba.czwek.units.UnitSystem
import kotlin.math.abs

class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
	val root: View = itemView.findViewById(R.id.suggestion)
	val icon: ImageView = itemView.findViewById(R.id.suggestion_image)
	val title: TextView = itemView.findViewById(R.id.suggestion_title)
	val changeOptions: TextView = itemView.findViewById(R.id.suggestion_change_options)
	val description: TextView = itemView.findViewById(R.id.suggestion_description)
	val feedName: TextView = itemView.findViewById(R.id.feed_name)
	val distance: TextView = itemView.findViewById(R.id.distance)
	val arrow: ImageView = itemView.findViewById(R.id.arrow)

	companion object {
		fun bind(
			queryable: Queryable,
			holder: BimbaViewHolder?,
			context: Context?,
			feeds: Map<String, FeedInfo>?,
			feedsSettings: FeedsSettings?,
			onClickListener: (Queryable) -> Unit,
			position: Location?,
			heading: Float?,
			showArrow: Boolean
		) {
			when (queryable) {
				is Stop -> bindStop(
					queryable,
					holder,
					context,
					feeds,
					feedsSettings,
					position,
					heading,
					showArrow
				)

				is Line -> bindLine(queryable, holder, context, feeds, feedsSettings)

				is PointOfInterest -> bindPointOfInterest(queryable, holder, context)
			}
			holder?.root?.setOnClickListener {
				onClickListener(queryable)
			}
		}

		fun bind(
			stopStub: StopStub,
			holder: BimbaViewHolder?,
			context: Context?,
			onClickListener: (StopStub) -> Unit
		) {
			holder?.title?.text = stopStub.name
			holder?.icon?.apply {
				setImageDrawable(stopStub.icon(context!!))
				contentDescription = context.getString(R.string.stop_content_description)
			}
			holder?.changeOptions?.text = when {
				stopStub.zone != "" && stopStub.onDemand -> context?.getString(
					R.string.stop_stub_on_demand_in_zone,
					stopStub.zone
				)

				stopStub.zone == "" && stopStub.onDemand -> context?.getString(R.string.stop_stub_on_demand)
				stopStub.zone != "" && !stopStub.onDemand -> context?.getString(
					R.string.stop_stub_in_zone,
					stopStub.zone
				)

				else -> ""
			}
			holder?.root?.setOnClickListener {
				onClickListener(stopStub)
			}
		}

		private fun bindStop(
			stop: Stop,
			holder: BimbaViewHolder?,
			context: Context?,
			feeds: Map<String, FeedInfo>?,
			feedsSettings: FeedsSettings?,
			position: Location?,
			heading: Float?,
			showArrow: Boolean
		) {

			if (showArrow && position != null && heading != null) {
				Location(null).apply {
					latitude = stop.stopPosition.positionLatitude
					longitude = stop.stopPosition.positionLongitude
				}.let {
					val angle =
						(360 + ((position.bearingTo(it) + 360).mod(360f)) - heading).mod(360f)
					val distance = position.distanceTo(it)
					holder?.arrow?.apply {
						setImageResource(R.drawable.arrow)
						rotation = angle
						visibility = View.VISIBLE
						contentDescription =
							context?.getString(R.string.arrow) ?: "Arrow pointing to the stop" // TODO add angle
					}
					holder?.distance?.apply {
						val us = UnitSystem.getSelected(context!!)
						text = us.toString(context, us.distanceUnit(Metre(distance.toDouble())))
						contentDescription =
							us.distanceUnit(Metre(distance.toDouble())).contentDescription(context, us.base)
						visibility = View.VISIBLE
					}
				}
			} else {
				holder?.arrow?.visibility = View.GONE
				holder?.distance?.visibility = View.GONE
			}

			holder?.icon?.apply {
				setImageDrawable(stop.icon(context!!))
				contentDescription = context.getString(R.string.stop_content_description)
			}
			holder?.title?.text = stop.stopName
			if ((feedsSettings?.activeFeedsCountAll() ?: 0) > 1 || (stop.feedID ?: "") == "transitous") {
				holder?.feedName?.visibility = View.VISIBLE
				holder?.feedName?.text = feeds?.get(stop.feedID)?.name ?: ""
			} else {
				holder?.feedName?.visibility = View.GONE
			}
			context?.let {
				stop.changeOptions(it, Stop.LineDecoration.fromPreferences(context)).let { changeOptions ->
					if (changeOptions.first.isEmpty()) {
						holder?.changeOptions?.visibility = View.GONE
					} else {
						holder?.changeOptions?.apply {
							text = changeOptions.first
							contentDescription = changeOptions.second
							visibility = View.VISIBLE
						}
					}
				}
			}
			if (stop.description.isNullOrBlank()) {
				holder?.changeOptions?.visibility = View.VISIBLE
				holder?.description?.visibility = View.GONE
			} else {
				holder?.changeOptions?.visibility = View.GONE
				holder?.description?.visibility = View.VISIBLE
				holder?.description?.text = stop.description
			}
		}

		private fun bindLine(
			line: Line,
			holder: BimbaViewHolder?,
			context: Context?,
			feeds: Map<String, FeedInfo>?,
			feedsSettings: FeedsSettings?
		) {
			holder?.icon?.apply {
				setImageDrawable(line.icon(context!!))
				contentDescription = line.type.name
				colorFilter = null
			}
			if ((feedsSettings?.activeFeedsCount() ?: 0) > 1) {
				holder?.feedName?.visibility = View.VISIBLE
				holder?.feedName?.text = feeds?.get(line.feedID)?.name ?: ""
			}
			holder?.title?.text = line.name
			holder?.changeOptions?.text = if (line.headsigns.size == 1) {
				context?.getString(
					R.string.line_headsign,
					line.headsigns[0].joinToString { it })
			} else {
				context?.getString(
					R.string.line_headsigns,
					line.headsigns[0].joinToString { it },
					line.headsigns[1].joinToString { it })
			}
			holder?.changeOptions?.contentDescription = if (line.headsigns.size == 1) {
				context?.getString(
					R.string.line_headsign_content_description,
					line.headsigns[0].joinToString { it })
			} else {
				context?.getString(
					R.string.line_headsigns_content_description,
					line.headsigns[0].joinToString { it },
					line.headsigns[1].joinToString { it })
			}
		}

		private fun bindPointOfInterest(
			poi: PointOfInterest,
			holder: BimbaViewHolder?,
			context: Context?
		) {
			holder?.icon?.apply {
				setImageDrawable(AppCompatResources.getDrawable(context!!, R.drawable.poi))
				contentDescription = context.getString(R.string.point_of_interest)
				colorFilter = null
			}
			holder?.feedName?.visibility = View.GONE
			holder?.title?.text = poi.poiName
			if (poi.poiDescription.isBlank()) {
				holder?.changeOptions?.visibility = View.VISIBLE
				holder?.description?.visibility = View.GONE
			} else {
				holder?.changeOptions?.visibility = View.GONE
				holder?.description?.visibility = View.VISIBLE
				holder?.description?.text = poi.poiDescription
			}
		}
	}
}


class BimbaResultsAdapter(
	private val inflater: LayoutInflater,
	private val context: Context?,
	private var queryables: List<Queryable>,
	private var position: Location?,
	private var heading: Float?,
	private var showArrow: Boolean,
	private val returnResult: Boolean = false
) :
	RecyclerView.Adapter<BimbaViewHolder>() {
	class DiffUtilCallback(
		private val oldQueryables: List<Queryable>,
		private val newQueryables: List<Queryable>,
		private val oldPosition: Location?,
		private val newPosition: Location?,
		private val oldHeading: Float?,
		private val newHeading: Float?,
		private val oldShowArrow: Boolean,
		private val newShowArrow: Boolean
	) : DiffUtil.Callback() {
		override fun getOldListSize() = oldQueryables.size

		override fun getNewListSize() = newQueryables.size

		override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
			val oldQueryable = oldQueryables[oldItemPosition]
			val newQueryable = newQueryables[newItemPosition]
			return if (oldQueryable::class.java != newQueryable::class.java) {
				false
			} else {
				// TODO to polymorphism
				when (oldQueryable) {
					is Line -> {
						assert(newQueryable is Line)
						oldQueryable.name == (newQueryable as Line).name // TODO compare line.ID when struct is updated
					}

					is Stop -> {
						assert(newQueryable is Stop)
						oldQueryable.code == (newQueryable as Stop).code
					}

					is PointOfInterest -> {
						assert(newQueryable is PointOfInterest)
						oldQueryable.poiName == (newQueryable as PointOfInterest).poiName
					}

					else -> false // XXX unreachable
				}
			}
		}

		override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
			val oldQueryable = oldQueryables[oldItemPosition]
			val newQueryable = newQueryables[newItemPosition]
			return when (oldQueryable) {
				is Line -> {
					assert(newQueryable is Line)
					val oldHeadsigns =
						oldQueryable.headsigns.joinToString { hsList -> hsList.joinToString { it } }
					val newHeadsigns =
						(newQueryable as Line).headsigns.joinToString { hsList -> hsList.joinToString { it } }

					oldQueryable.name == newQueryable.name && oldQueryable.type == newQueryable.type &&
							oldQueryable.colour == newQueryable.colour && oldHeadsigns == newHeadsigns
				}

				is Stop -> {
					assert(newQueryable is Stop)
					val oldChangeOptions = oldQueryable.changeOptionsString()
					val newChangeOptions = (newQueryable as Stop).changeOptionsString()

					oldQueryable.stopName == newQueryable.stopName && oldChangeOptions == newChangeOptions &&
							oldPosition?.latitude == newPosition?.latitude &&
							oldPosition?.longitude == newPosition?.longitude &&
							oldHeading == newHeading &&
							oldShowArrow == newShowArrow
				}

				is PointOfInterest -> {
					assert(newQueryable is PointOfInterest)

					oldQueryable.poiPosition == (newQueryable as PointOfInterest).poiPosition &&
							oldQueryable.poiName == newQueryable.poiName &&
							oldQueryable.poiDescription == newQueryable.poiName
				}

				else -> false // XXX unreachable
			}
		}
	}

	var feeds: Map<String, FeedInfo>? = null

	init {
		feeds = runBlocking(Dispatchers.IO) {
			context?.let {
				SettingsRepository()
					.getFeedInfos(it)
					.toSet()
					.mapNotNull { feedInfoItem ->
						when (feedInfoItem) {
							is FeedInfo -> {
								feedInfoItem
							}

							else -> null
						}
					}.associate { Pair(it.id, it) }
			}
		}
	}

	var feedsSettings: FeedsSettings? = null
	private val onClickListener: ((Queryable) -> Unit) = {
		when (it) {
			is Stop -> {
				if (returnResult) {
					(context as ResultsActivity).returnResult(it)
				} else {
					val intent = Intent(context, DeparturesActivity::class.java).apply {
						putExtra("code", it.code)
						putExtra("name", it.stopName)
						putExtra("feedID", it.feedID)
					}
					context!!.startActivity(intent)
				}
			}

			is Line -> {
				if (returnResult) {
					// TODO shouldn't happen
				} else {
					val intent = Intent(context, LineGraphActivity::class.java).apply {
						putExtra("lineName", it.name)
						putExtra("lineID", it.id)
						putExtra("feedID", it.feedID)
					}
					context!!.startActivity(intent)
				}
			}

			is PointOfInterest -> {
				if (returnResult) {
					(context as ResultsActivity).returnResult(it)
				} else {
					// TODO shouldn't happen
				}
			}
		}
	}

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaViewHolder {
		val rowView = inflater.inflate(R.layout.result, parent, false)
		return BimbaViewHolder(rowView)
	}

	override fun onBindViewHolder(holder: BimbaViewHolder, position: Int) {
		BimbaViewHolder.bind(
			queryables[position],
			holder,
			context,
			feeds,
			feedsSettings,
			onClickListener,
			this.position,
			heading,
			showArrow
		)
	}

	override fun getItemCount(): Int = queryables.size

	fun update(
		queryables: List<Queryable>?,
		position: Location?,
		heading: Float?,
		showArrow: Boolean
	) {
		val diff = DiffUtil.calculateDiff(
			DiffUtilCallback(
				this.queryables,
				queryables ?: emptyList(),
				this.position,
				position,
				this.heading,
				heading,
				this.showArrow,
				showArrow
			)
		)
		this.position = position
		this.heading = heading
		this.showArrow = showArrow
		this.queryables = queryables ?: emptyList()
		diff.dispatchUpdatesTo(this)
	}

	fun update(
		heading: Float?,
	) {
		if (abs((heading ?: 0f) - (this.heading ?: 0f)) < 15) {
			return
		}
		val diff = DiffUtil.calculateDiff(
			DiffUtilCallback(
				queryables,
				queryables,
				position,
				position,
				this.heading,
				heading,
				showArrow,
				showArrow
			)
		)
		this.heading = heading
		diff.dispatchUpdatesTo(this)
	}

	fun update(
		queryables: List<Queryable>?,
		position: Location?,
		showArrow: Boolean
	) {
		val diff = DiffUtil.calculateDiff(
			DiffUtilCallback(
				this.queryables,
				queryables ?: emptyList(),
				this.position,
				position,
				heading,
				heading,
				this.showArrow,
				showArrow
			)
		)
		this.position = position
		this.showArrow = showArrow
		this.queryables = queryables ?: emptyList()
		diff.dispatchUpdatesTo(this)
	}

	fun click(position: Int) {
		onClickListener(queryables[position])
	}

	fun isNullOrEmpty(): Boolean = queryables.isEmpty()

	fun size(): Int = queryables.size
}